#!/usr/bin/perl

# FIXME: authenticated commenters aren't created

use strict;
use warnings;

use lib 'extlib', 'lib';
use Data::Dumper;
use Getopt::Long;
use MT;
use MT::Author;
use MT::Permission;

my $mt = MT->new() or die "No MT object " . MT->errstr();

my $blogs   = 1;
my $destroy = 0;
my $authors = 3;
my $entries = 500;
my $cats    = 5;
# this can be used to scale the default amount of data generated
my $weight_factor = 1;
my $bypass_confirm = 0;
my $help    = 0;
my $silent  = 0;
my $years   = 1;

my $result = GetOptions("authors=i"    => \$authors,
                        "blogs=i"      => \$blogs,
                        "categories=i" => \$cats,
                        "entries=i"    => \$entries,
                        "weight=i"     => \$weight_factor,
                        "destroy"      => \$destroy,
                        "yes-really"   => \$bypass_confirm,
                        "help|?"       => \$help,
                        "silent"       => \$silent,
                        "years=i"      => \$years,
);

if ($help) {
    eval { require Pod::Usage };
    die "No Pod::Usage available. Use the Source, Luke." if $@;
    Pod::Usage::pod2usage(-exitstatus => 1);
}

use vars qw($this_blog $this_iter $this_obj $this_parent $tmpl_list);

# define objects and such...
use constant RAND_BOOL => [ 0, 1 ];

my @word_pool = qw(
    cool tech blogging politics interesting video
    perl python ruby web2.0 sixapart reference link audio
    php javascript voip news
);

# Randomize the random number generator to make things nice and consistent.
srand(1);

# these are all the numbers that control how many of what are generated
my $blog_count             = sub { $blogs              * $weight_factor };
my $authors_per_blog       = sub { $authors            * $weight_factor };
my $categories_per_blog    = sub { $cats + int(rand(10)) * $weight_factor };
my $entries_per_blog       = sub { $entries + int(rand(30)) * $weight_factor };
my $notifications_per_blog = sub { 3   + int(rand(20))                  };
my $bans_per_blog          = sub { 2   + int(rand(10))                  };
my $comments_per_entry     = sub {       int(rand(10)) * $weight_factor };
my $tags_per_entry         = sub {       int(rand(5))  * $weight_factor };
my $pings_per_trackback    = sub {       int(rand(10)) * $weight_factor };
my %defs = (
    'MT::Blog' => {
        table => 'blog',
        as_of => 1.0,
        needs => [
            'MT::Author'       => $authors_per_blog,
            'MT::Category'     => $categories_per_blog,
            'MT::Entry'        => $entries_per_blog,
            'MT::Template'     => \&template_loader,
            'MT::Notification' => $notifications_per_blog,
            'MT::IPBanList'    => $bans_per_blog,
        ],
        fields => [
            field('name', \&random_blog_name),
            field('description', sub { random_paragraph(int(rand(1))) } ),
            field('archive_type', [ 'Individual,Category,Monthly',
                'Individual,Monthly', 'Daily,Category,Monthly' ]),
            field('archive_type_preferred', sub {
                my @at = split /,/, $this_obj->archive_type;
                $at[int(rand(scalar @at))]
            } ),
            field('site_path', sub { "/www/blog_$this_iter" } ),
            field('archive_path', sub { $this_obj->site_path . '/archives' }),
            field('site_url', sub { "http://www.blog${this_iter}.com/" } ),
            field('archive_url', sub { $this_obj->site_url . 'archives/' }),
            field('days_on_index', [ 7, 10, 14 ]),
            field('entries_on_index', [ 10, 15, 20 ], 3.2),
            field('file_extension', [ '', '', 'html', 'asp', 'jsp', 'php' ] ),
            field('email_new_comments', RAND_BOOL),
            field('allow_comment_html', RAND_BOOL),
            field('autolink_urls', RAND_BOOL),
            field('server_offset', [ -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5 ]),
            field('ping_blogs', RAND_BOOL, 2.5),
            field('ping_others', RAND_BOOL, 2.5),
            field('autodiscover_links', RAND_BOOL, 2.5),
            field('convert_paras', RAND_BOOL),
            field('convert_paras_comments', RAND_BOOL),
            field('sanitize_spec', [ 'a href,br/,p', '' ], 2.6),
            field('cc_license', sub {
                if (MT->version_number < 3.16) {
                    [ '', 'by-nc-sa', 'nd', 'by', 'pd', 'sa',
                      'by-nd-nc', 'by-nc' ];
                } else {
                    [ '', '' ], # FIXME: URLs for new cc licenses
                }
            }, 2.6),
            field('is_dynamic', 0, 2.6),
            field('require_comment_emails', RAND_BOOL, 3.0),
            field('allow_unreg_comments', RAND_BOOL, 3.0),
            field('allow_reg_comments', RAND_BOOL, 3.0),
            field('manual_approve_commenters', RAND_BOOL, 3.0),
            field('old_style_archive_links', RAND_BOOL, 3.0),
            field('moderate_unreg_comments', RAND_BOOL, 3.0),
            field('remote_auth_token', [ '', 'some_remote_token' ], 3.0),
            field('ping_technorati', RAND_BOOL, 3.1),
            field('children_modified_on', undef, 3.1),
            field('custom_dynamic_templates',
                [ 'none', 'archives', 'custom' ], 3.1),
        ],
    },
    'MT::Entry' => {
        table => 'entry',
        as_of => 1.0,
        needs => [
            'MT::Comment' => sub { $this_obj->allow_comments ? $comments_per_entry->() : 0 },
            'MT::Trackback' => sub { $this_obj->allow_pings ? 1 : 0 },
            #'MT::Placement' => [ 0, 1, 1, 1, 2 ],
        ],
        fields => [
            field('blog_id', sub { $this_blog->id } ),
            field('author_id', \&random_author),
            field('title', \&random_title),
            field('text', sub { random_paragraph(2+int(rand(3)))} ),
            field('text_more', sub { random_paragraph(int(rand(10))) } ),
            field('excerpt', ''),
            field('status', [ 1, 2, 2, 2, 2, 4 ]),
            field('keywords', '', 2.5),
            field('tangent_cache', '', 2.5),
            field('allow_pings', [ 0, 1 ]),
            field('allow_comments', [ 0, 1, 1, 1 ]),
            field('convert_breaks', sub {
                MT->version_number < 2.6 ? [ 0, 1 ] : [ '__default__', '0' ]
            }),
            field('created_on', \&random_date, 1.0 ),
            field('basename', undef, 3.0), # auto-set upon save
            field('tags', \&assign_random_tags, 3.3 ),
            field('categories', \&assign_random_categories, 1.0 ),
        ],
    },
    'MT::Author' => {
        table => 'author',
        as_of => 1.0,
        needs => [
            'MT::Permission' => \&permission_check,
        ],
        fields => [
            field('name', \&random_username),
            field('nickname', \&random_name),
            field('email', \&random_email),
            field('url', \&random_url),
            field('type', sub { ref $this_parent eq 'MT::Comment' ? 2 : 1 }, 3.0),
            field('password', sub { $this_obj->type == 1 ? crypt('Nelson', 'xx') : '' }),
            field('preferred_language', sub { $this_obj->type == 1 ? ($this_obj->name eq 'Melody' ? 'en_US' : [ 'en_US', 'en_US', 'en-us', 'fr', 'de' ]) : undef }, 2.5),
            field('can_create_blog', sub { $this_obj->type == 1 ? RAND_BOOL : undef }),
            field('is_superuser', sub { $this_obj->type == 1 && $this_iter == 1 ? 1 : undef }, 3.2),
            field('can_view_log', sub { $this_obj->type == 1 ? RAND_BOOL : undef }),
            field('hint', sub { $this_obj->type == 1 ? 'hint' : undef } ),
            field('public_key', undef),
            field('api_password', sub { $this_obj->type == 1 ? 'Nelson' : undef }, 3.2),
            field('remote_auth_username', undef, 3.0),
            field('remote_auth_token', undef, 3.0),
        ],
    },
    'MT::Category' => {
        table => 'category',
        as_of => 1.0,
        needs => [
            'MT::Trackback' => [ 0, 0, 0, 0, 0, 1 ],
        ],
        fields => [
            field('blog_id', sub { $this_blog->id }),
            field('label', \&random_category),
            field('author_id', ),
            field('description', sub { random_paragraph(int(rand(1))) }),
            field('allow_pings', [ 0, 0, 0, 0, 1 ], 1.0), # FIXME: version #
            field('parent', [ 0, 0, 0, \&random_category_id ], 3.1),
        ],
    },
    'MT::Permission' => {
        table => 'permission',
        as_of => 1.0,
        fields => [
            field('author_id', sub { $this_parent->id } ),
            field('blog_id', sub { $this_blog->id } ),
            field('permissions', sub { $this_parent->type == 1 ? ($this_iter == 1 ? 14334 : &random_role ) : [ 0, 1 ] } ),
            field('entry_prefs', undef),
        ],
    },
    'MT::Comment' => {
        table => 'comment',
        as_of => 1.0,
        needs => [
            'MT::Author' => [ 0, 0, 0, 0, 1 ],
        ],
        fields => [
            field('blog_id', sub { $this_parent->blog_id }),
            field('entry_id', sub { $this_parent->id }),
            field('author', \&random_name),
            field('commenter_id', \&random_commenter, 3.0),
            field('email', \&random_email),
            field('url', \&random_url),
            field('text', sub { random_paragraph(1+int(rand(2))) } ),
            field('ip', \&random_ip),
            field('visible', [ 0, 1, 1, 1, 1 ], 3.0),
            field('junk_status', [ -1, 1, 1, 1, 1 ], 3.2),
            field('junk_score', sub { $this_obj->junk_status == -1 ? rand(2) * -1 : 0 }, 3.2),
            field('junk_log', sub { $this_obj->junk_status == -1 ? 'Random (' . $this_obj->junk_score . "): Randomly scored as junk\n" : '' }, 3.2),
        ],
    },
    'MT::Trackback' => {
        table => 'trackback',
        as_of => 1.2,   # ??
        needs => [
            'MT::TBPing' => $pings_per_trackback,
        ],
        fields => [
            field('blog_id', sub { $this_parent->blog_id }),
            field('title', \&random_title),
            field('description', undef),
            field('rss_file', undef),
            field('url', undef),
            field('entry_id', sub { ref $this_parent eq 'MT::Entry' ? $this_parent->id : 0 }),
            field('category_id', sub { ref $this_parent eq 'MT::Category' ? $this_parent->id : 0 }),
            field('is_disabled', 0),
            field('passphrase', undef),
        ],
    },
    'MT::TBPing' => {
        table => 'tbping',
        as_of => 1.2,   # ??
        fields => [
            field('blog_id', sub { $this_parent->blog_id }),
            field('tb_id', sub { $this_parent->id }),
            field('title', \&random_title),
            field('excerpt', \&random_paragraph ),
            field('source_url', \&random_url),
            field('ip', \&random_ip),
            field('blog_name', \&random_blog_name),
            field('visible', [ 0, 1, 1, 1, 1 ], 3.0),
            field('junk_status', [ -1, 1, 1, 1, 1 ], 3.2),
            field('junk_score', sub { $this_obj->junk_status == -1 ? rand(2) * -1 : 0 }, 3.2),
            field('junk_log', sub { $this_obj->junk_status == -1 ? 'Random (' . $this_obj->junk_score . "): Randomly scored as junk\n" : '' }, 3.2),
        ],
    },
    'MT::Template' => {
        table => 'template',
        as_of => 1.0,
#        fields => [
#            field('build_dynamic', [], 3.1),
#        ],
    },
    'MT::TemplateMap' => {
        table => 'templatemap',
        as_of => 2.0,
    },
    'MT::IPBanList' => {
        table => 'ipbanlist',
        as_of => 1.4,  # ??
        fields => [
            field('blog_id', sub { $this_parent->id } ),
            field('ip', \&random_ip),
        ],
    },
    'MT::Log' => {
        table => 'log',
        as_of => 1.0,
#        fields => [
#            field('blog_id', [], 3.2),
#        ],
    },
    'MT::Session' => {
        table => 'session',
        as_of => 3.0,
    },
    'MT::Notification' => {
        table => 'notification',
        as_of => 1.0,
        fields => [
            field('blog_id', sub { $this_parent->id } ),
            field('name', \&random_name),
            field('email', \&random_email),
            field('url', \&random_url),
        ],
    },
    'MT::Placement' => {
        table => 'placement',
        as_of => 2.0,
    },
    'MT::FileInfo' => {
        table => 'fileinfo',
        as_of => 3.1,
    },
    'MT::PluginData' => {
        table => 'plugindata',
        as_of => 2.6,
    },
    'MT::Tag' => {
        table => 'tag',
        as_of => 3.3,
    },
    'MT::ObjectTag' => {
        table => 'objecttag',
        as_of => 3.3,
    },
#    'MT::Config' => {
#        table => 'config',
#        as_of => 3.2,
#    },
);

if ($destroy) {
    unless ($bypass_confirm) {
        print <<'WARNING';
** WARNING **

You've chosen to clear all existing data in your database and populate
it with random, generated data. The system administrator login will be
Melody/Nelson. This tool is mainly used for testing MT performance and
usability with varying sizes of data.

To continue, type OK and press enter.
WARNING

        my $confirm = <STDIN>;
        chomp $confirm;
        exit unless $confirm eq 'OK';
    }
    clean();
}

# kick off the build process
build( 'MT::Blog', $blog_count->(), 0);

sub field {
    my ($name, $inputs, $as_of) = @_;
    my $def = { inputs => $inputs };
    $def->{as_of} = $as_of if defined $as_of;
    ( $name, $def );
}

sub clean {
    # CLEAR ANY EXISTING DATA

    print "Clearing existing data...\n" unless $silent;
    my $driver = MT::Object->driver();
    if (my $dbh = $driver->rw_handle) {
        my @tables = map { $defs{$_}{table} } keys %defs;
        foreach (@tables) {
            print "\tWiping $_\n" unless $silent;
            $dbh->do("delete from mt_$_");
        }
        if ($driver->isa('MT::ObjectDriver::Driver::DBD::Pg')) {
            # reset sequences
            $dbh->do("drop sequence mt_${_}_id") foreach @tables;
            $dbh->do("create sequence mt_${_}_id") foreach @tables;
        }
    }
}

sub build {
    my ($pkg, $num, $depth) = @_;

    my $def = $defs{$pkg};

    return unless $def->{as_of} <= MT->version_number();

    print(("\t" x $depth) . "Creating $num $pkg objects...\n") unless $silent;

    local $this_obj = $this_obj;
    local $this_parent = $this_obj;
    local $this_iter;
    for (my $i = 0; $i < $num; $i++) {
        $this_iter = $i + 1;  # always indexed from 1...

        my $obj;
        # special case for MT::Author...
        if ($pkg eq 'MT::Author') {
            my $name = random_username();
            my $user = MT::Author->load({name => $name, type => 1});
            if ($user) {
                $obj = $user;
            }
        }
        unless ($obj) {
            no strict 'refs';
            no warnings;
            eval("require $pkg") unless defined *{"$pkg::"};
            $obj = $pkg->new();
        }
        $this_obj = $obj;
        if (!$obj->id) {
            my @fields = @{$def->{fields}};
            while (my ($fld, $fld_def) = (shift @fields, shift @fields)) {
                if ((!exists $fld_def->{as_of}) || ($fld_def->{as_of} && ($fld_def->{as_of} <= $MT::VERSION))) {
                    my $input = $fld_def->{inputs};
                    while (ref $input) {
                        if (ref $input eq 'ARRAY') {
                            $input = $input->[int(rand(scalar @$input))];
                        }
                        if (ref $input eq 'CODE') {
                            $input = $input->();
                        }
                    }
                    print "\tsetting $fld to [$input]\n" if !$silent && defined $input;
                    $obj->$fld($input) if defined $input;
                }
                last unless @fields;
            }
            $obj->save or die $obj->errstr;
        }

        if ($pkg eq 'MT::Blog') {
            $this_blog = $obj; # assign for use in other objects...
        }

        # process needs...
        if (my $needs = $def->{needs}) {
            my @needs = @$needs;
            while (my ($pkg, $num) = (shift @needs, shift @needs)) {
                while (ref $num) {
                    if (ref $num eq 'ARRAY') {
                        $num = $num->[int(rand(scalar @$num))];
                    }
                    if (ref $num eq 'CODE') {
                        $num = $num->();
                    }
                }
                if ($num) {
                    build($pkg, $num, $depth+1);
                }
                last unless @needs;
            }
        }
    }
}

sub permission_check {
    my $author = $this_obj;
    my $blog = $this_blog;
    require MT::Permission;
    my $num = MT::Permission->count({ author_id => $author->id,
        blog_id => $blog->id });
    $num ? 0 : 1;
}

sub template_loader {
    my $blog = $this_blog;
    print "\tCreating templates...\n" unless $silent;
    $blog->create_default_templates();
    0;
}

## some random routines

sub random_name {
    $this_iter == 1 ? 'Melody Nelson' : 'Joe ' . $this_iter;
}

sub random_email {
    "user" . $this_iter . '@gmail.com';
}

sub random_paragraph {
    my $num = shift;
    $num ||= 1 unless defined $num;
    ("Lorem ipsum blah blah blah.\n\n" x $num) || '';
}

sub random_username {
    $this_iter == 1 ? 'Melody' : 'user' . $this_iter;
}

sub random_blog_name {
    'Weblog ' . $this_iter;
}

sub random_category_id {
}

sub random_category {
    'Category ' . $this_iter;
}

sub random_title {
    'Title for ' . (ref $this_obj) . ' #' . $this_iter;
}

sub random_commenter {
    my @c = MT::Author->load({ type => 2 });
    return unless @c;
    my $c = $c[int(rand(scalar @c))];
    $c->id;
}

sub random_author {
    my @a = MT::Author->load({ type => 1 });
    my $a = $a[int(rand(scalar @a))];
    $a->id;
}

sub random_role {
#        [ 1, 'comment', 'Post Comments', ],
#        [ 4096, 'administer_blog', 'Blog Administrator'],
#        [ 2, 'post', 'Post', ],
#        [ 4, 'upload', 'Upload File', ],
#        [ 8, 'edit_all_posts', 'Edit All Posts', ],
#        [ 16, 'edit_templates', 'Manage Templates', ],
##       [ 32, 'edit_authors', 'Edit Authors & Permissions', ],
#        [ 64, 'edit_config', 'Configure Weblog', ],
#        [ 128, 'rebuild', 'Rebuild Files', ],
#        [ 256, 'send_notifications', 'Send Notifications', ],
#        [ 512, 'edit_categories', 'Manage Categories', ],
#        [ 1024, 'edit_notifications', 'Manage Notification List' ],
#        [ 2048, 'not_comment', ''],  # not a real permission but a denial thereeof
#        [ 8192, 'view_blog_log', 'View Activity Log For This Weblog']
    my @roles;
    if ($this_parent->type == 2) {
        @roles = ("'comment'", "'comment'", "'comment'", "'comment'", "'not_comment'");
    } else {
        if (MT->version_number >= 3.2) {
            push @roles, ("'post'", "'post'", "'post'", "'post'");
            push @roles, ("'post','upload'");
            push @roles, ("'post','upload','edit_all_posts'");
            push @roles, ("'edit_templates','edit_all_posts','post','upload'");
            push @roles, ("'edit_config','edit_templates','edit_all_posts','post','upload'");
            push @roles, ("'edit_categories','edit_config','edit_templates','edit_all_posts','post','upload'");
            push @roles, ("'view_blog_log','edit_config'");
        } else {
            push @roles, ("'post'", "'post'", "'post'", "'post'");
            push @roles, ("'post','upload'");
            push @roles, ("'edit_all_posts','post','upload'");
            push @roles, ("'edit_templates','edit_all_posts','post','upload'");
            push @roles, ("'edit_config','edit_templates','edit_all_posts','post','upload'");
            push @roles, ("'edit_categories','edit_templates','edit_all_posts','post','upload'");
            push @roles, ("'edit_config','edit_authors','edit_all_posts'");
        }
    }
    $roles[int(rand(scalar @roles))];
}

sub random_url {
    'http://www.example.com/user' . $this_iter . '/';
}

sub random_ip {
    "192.168.0." . $this_iter;
}

sub assign_random_tags {
    my @tags;
    my $count = $tags_per_entry->();
    for (my $i = 0; $i < $count; $i++) {
        push @tags, $word_pool[rand(scalar @word_pool)];
    }
    $this_obj->tags(@tags);
    undef;
}

sub assign_random_categories {
    my @count = (0, 1, 1, 1, 1, 2);

    # this is the last field; go ahead and save this object so
    # we have an entry id.
    $this_obj->save;

    my $count = $count[int(rand(scalar @count))];
    require MT::Category;
    require MT::Placement;
    my @cats = MT::Category->load({ blog_id => $this_obj->blog_id });
    my %used;
    $count = scalar @cats if scalar @cats < $count;
    for (my $i = 0; $i < $count; $i++) {
        my $cat = $cats[int(rand(scalar @cats))];
        next if $used{$cat->id};
        my $place = MT::Placement->new;
        $place->category_id($cat->id);
        $place->blog_id($this_obj->blog_id);
        $place->entry_id($this_obj->id);
        $place->is_primary(%used ? 0 : 1);
        $place->save or die $place->errstr;
        $used{$cat->id} = 1;
    }
    undef;
}

sub random_date {
    my @date = localtime(time - int(rand($years * 365 * 24 * 60 * 60)));
    return sprintf("%04d%02d%02d%02d%02d%02d",
        $date[5] + 1900, $date[4] + 1, $date[3], $date[2], $date[1], $date[0]
    );
}

1;

__END__

=head1 NAME

populate - A utility for creating pseudorandom weblog data.

=head1 SYNOPSIS

    # Create 3 'random' weblogs with ~500 entries each
    tools/populate --blogs 3 --destroy --yes-really

    # Adds an addtional weblog
    tools/populate --blogs 1

=head1 OPTIONS
            
=over 4

=item B<--destroy>

Wipes your existing database before populating it. This switch is confirmed
before execution.  If this is done, the system admin becomes Melody Nelson.

=item --yes-really

Bypasses the confirmation for the '--destroy' switch.

=item --blogs E<lt>nE<gt>

Lets you specify the number of weblogs to create (default is 1).

=item --authors E<lt>nE<gt>

Specifies the number of user records to create (default is 3).

=item --entries E<lt>nE<gt>

Specifies the number of entries (approximately) to create (default is 500).

=item --categories E<lt>nE<gt>

Specifies the number of categories (approximately) to create (default is 5).

=item --years E<lt>nE<gt>

Specifies the number of years to post-date entry creation (default is 1).

=back
